// SPDX-License-Identifier: GPL-2.0 or GPL-3.0
// Copyright © 2018-2019 Ariadne Devos
/* sHT -- accept connections from passive sockets (Unix) */

#include "worker.h"

#include <sHT/stream.h>
#include <sHT/accept.h>
#include <sHT/bugs.h>
#include <sHT/compiler.h>
#include <sHT/test.h>

#include <errno.h>
#include <stddef.h>
#include <sys/epoll.h>
#include <sys/socket.h>

static const struct sHT_bug_option sHT_accept_bugs[] =
  { { .code = EBADFD, .msg = "passive socket not a fd" },
    { .code = EINVAL, .msg = "socket not listening" },
    { .code = ENOTSOCK, .msg = "not a socket" } };

static int
sHT_sys_accept(int fd, int flags)
{
	int new_fd = accept4(fd, NULL, NULL, flags);
	if (sHT_lt0(new_fd))
		new_fd = -errno;
	return new_fd;
}

__attribute__((nonnull (1, 2)))
static _Bool
sHT_try_accept(struct sHT_worker *worker, struct sHT_task_accept *task)
{
	/* It is called a subtask or child task, but sHT has no concept
	   of any such DAG. */
	struct sHT_task_stream *subtask = sHT_accept_subtask(worker, task);
	if (sHT_null_p(subtask))
		return 1;
	int fd;
retry:
	/* XXX: use errno correctly */
	fd = sHT_sys_accept(task->accept.fd, SOCK_NONBLOCK | SOCK_CLOEXEC);
	if (!sHT_lt0(fd)) {
		/* Only set this field. It is up to sHT_accept_subtask and
		sHT_accept_logic to fill in other fields. */
		subtask->stream.fd = fd;
		sHT_accept_logic(worker, task, subtask);
		return 0;
	}
	/* TODO: possibly unportable, assumes Linux
	   Probably, only a few cases should be added. */
	int err = errno;
	switch(err) {
#if EAGAIN != EWOULDBLOCK
	case EAGAIN: /* fallthrough */
#endif
	case EWOULDBLOCK:
		sHT_hide_var(err);
		task->task.epollflags &= ~EPOLLIN;
		sHT_accept_abort(worker, task, subtask);
		return 1;
	case EMFILE:
		sHT_hide_var(err);
		worker->flags |= sHT_WORKER_LIMIT_FDS;
		return 1;
	case ENFILE:
		sHT_hide_var(err);
		/* not that we can really do anything about it,
		   but provide a diagnostic anyway */
		worker->flags |= sHT_WORKER_SYSTEM_FDS;
		return 1;
	case ENOBUFS: /* fallthrough */
	case ENOMEM:
		sHT_hide_var(err);
		worker->flags |= sHT_WORKER_OOM;
		return 1;
	case EINTR:
		sHT_hide_var(err);
		goto retry;
	case EPROTO: /* fallthrough */
	case ECONNABORTED: /* fallthrough */
	/* firewall rules */
	case EPERM: /* fallthrough */
		sHT_hide_var(err);
		return 1;
	/* Linux accept4(2): Various Linux kernels can return other
	   errors ... */
	case ENOSR: /* fallthrough */
	case ESOCKTNOSUPPORT: /* fallthrough */
	case EPROTONOSUPPORT: /* fallthrough */
	case ETIMEDOUT:
		sHT_hide_var(err);
		return 1;
	/* From Linux accept4(2) Error handling: */
	case ENETDOWN: /* fallthrough */
	case ENOPROTOOPT: /* fallthrough */
	case EHOSTDOWN: /* fallthrough */
	case ENONET: /* fallthrough */
	case EHOSTUNREACH: /* fallthrough */
	case EOPNOTSUPP: /* fallthrough */
	case ENETUNREACH:
		sHT_hide_var(err);
		return 1;
	default:
		sHT_hide_var(err);
		sHT_bug(sHT_accept_bugs, err);
		return 0;
	}
}

void
sHT_accept_task(struct sHT_worker *worker, struct sHT_task_accept *task)
{
	if (!sHT_and_any(task->task.epollflags, EPOLLIN))
		/* TODO: other values */
		return;
	/* Something you should now when debugging:
	   if N connections are active,
	   you have allocated enough tasks, papers, etc. to handle N
	   connections,
	   but there is a connection waiting,
	   sHT_try_accept will nevertheless try to accept it,
	   but as there are not enough resources, sHT_TASK_* will be set.

	   So make sure that, in test cases, you allow an error message in
	   this case (which I did not). */
	for (int i = 0; sHT_gt(task->accept.max_accept_iterations, i); i++) {
		_Bool err = sHT_try_accept(worker, task);
		if (sHT_nonzero_p(err))
			return;
	}
}

